package org.reborn.engine.render.util

import javax.media.opengl._
import _root_.javax.media.opengl.GL
import _root_.javax.media.opengl.GL._
import _root_.javax.media.opengl.GL2
import _root_.javax.media.opengl.GL2._
import _root_.javax.media.opengl.GL2ES2
import _root_.javax.media.opengl.GL2ES2._
import _root_.javax.media.opengl.GL2GL3
import _root_.javax.media.opengl.GL2GL3._
import _root_.javax.media.opengl.fixedfunc.GLPointerFunc._

import org.reborn.engine.model.mesh.Mesh
import org.reborn.engine.model.mesh.DrawableMesh
import org.reborn.engine.model.mesh.VertexData
import org.reborn.engine.util.Registry
import org.reborn.engine.enums._
import ArrayType._
import FaceMode._
import UpdateFrequency._

import java.nio.Buffer
import java.nio.ByteBuffer
import java.nio.IntBuffer
import java.nio.FloatBuffer
import java.nio.ByteOrder

object RenderMeshVertexBufferObject extends RenderMeshSupport{
  protected override val functionList = List(
    "glDrawElements",
    "glVertexPointer",
    "glNormalPointer",
    "glColorPointer",
    "glTexCoordPointer",
    "glClientActiveTexture",
    "glGenBuffersARB",
  	"glBindBufferARB",
  	"glBufferDataARB",
  	"glDeleteBuffersARB")
  
  def createRenderer:RenderMesh = new RenderMeshVertexBufferObject();
}

class VertexArrayInfo(val aType:ArrayType,val verts:Int,val pPVertex:Int,val vboId:Int);
class MeshInfo(var arrayInfoMap:Map[ArrayType,VertexArrayInfo], 
               var faceVboId:Int, 
               var faces:Int, 
               var vertsPerFace:Int, 
               var faceMode:Int,
               var updateFreq:Int)

class RenderMeshVertexBufferObject extends RenderMesh{
  protected val meshRegistry:Registry[MeshInfo] = Registry.SimpleRegistry;
  
  def register(gl:GL2,mesh:Mesh,updateFreq:UpdateFrequency):Int = {
    val glUpdateFreq = getOpenGLUpdateFrequency(updateFreq)
    
    val (vboMap,faceBOId) = createMeshBuffers(gl,mesh,glUpdateFreq)
    
    //turn off vbos
    gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    
    //Keep track of the mesh and each vertex buffer object that we created
    meshRegistry.register(
      new MeshInfo(vboMap,
                   faceBOId,
                   mesh.faces,
                   mesh.verticesPerFace,
                   getFaceMode(mesh),
                   glUpdateFreq));
  }
  
  def unregister(gl:GL2,meshID:Int):Unit = {
    val registeredMesh = meshRegistry.get(meshID)
    if(registeredMesh.isEmpty)
      return
    
    val meshInfo = registeredMesh.get 
    
    deleteMeshBuffers(gl,meshInfo)
    
    //remove the mesh from our registry
    meshRegistry.unregister(meshID);
  }
  
  def render(gl:GL2,id:Int):Unit = {
    val registeredMesh = meshRegistry.get(id)
    if(registeredMesh.isEmpty)
      return
    
    //Retrieve the mesh that was registered previously
    val meshInfo = meshRegistry.get(id).get

    //Determine what types of faces we are drawing (Points,Lines,Tris,Quads)
    val faceMode = meshInfo.faceMode
    
    //Bind all available buffers to be used when we call drawElements
    enableAvailableClientStates(gl,meshInfo);
    
    //Bind the face index array and call drawElements
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, meshInfo.faceVboId);
    val faceIndices = meshInfo.faces * meshInfo.vertsPerFace
    gl.glDrawElements(faceMode, faceIndices, GL_UNSIGNED_INT, 0);	//Render the faces
    
    //Disable/unbind any buffers that we enabled before
    disableEnabledClientStates(gl,meshInfo)		
    
    //Finally, turn off vbos
    gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  }
  
  def update(gl:GL2,id:Int,newMesh:Mesh):Boolean = {
    //Retrieve the mesh that was registered previously
    val meshInfoOpt = meshRegistry.get(id)
    if(meshInfoOpt isEmpty) 
      return false;

    val meshInfo = meshInfoOpt.get
    
    //If anything about the mesn's structure has changed, then create all new data
    if(meshDifferent(meshInfo,newMesh)){
      deleteMeshBuffers(gl,meshInfo)
      val (arrayInfoMap,vboId) = createMeshBuffers(gl,newMesh,meshInfo.updateFreq)
      meshInfo.arrayInfoMap = arrayInfoMap
      meshInfo.faceVboId = vboId
    }
    //Otherwise, update the existing VBO data
    else{
      val aTypes = newMesh.getAvailableArrayTypes
      for(aType <- aTypes){
        val data = newMesh(aType)
        val sizeInBytes =  data.arraySize * 4
        updateBufferData(gl,
                      GL_ARRAY_BUFFER,
                      meshInfo.arrayInfoMap(aType).vboId,
                      data.getDataBuffer)
      }
      
      val sizeInBytes = newMesh.faces * newMesh.verticesPerFace * 4
      updateBufferData(gl,
                    GL_ELEMENT_ARRAY_BUFFER,
                    meshInfo.faceVboId,
                    newMesh.getFacesBuffer)
    }
    
    //Finally, turn off vbos
    gl.glBindBuffer(GL_ARRAY_BUFFER, 0);
    gl.glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    
    return true;
  }
  
  def updateArrayData(gl:GL2,id:Int,fromMesh:Mesh,aType:ArrayType):Boolean = {
    //Retrieve the mesh that was registered previously
    val meshInfoOpt = meshRegistry.get(id)
    if(meshInfoOpt isEmpty) return false;
    val meshInfo = meshInfoOpt.get
    
    val aVboId = meshInfo.arrayInfoMap(aType).vboId
    val vArray = fromMesh(aType)
    
    updateBufferData(gl,GL_ARRAY_BUFFER,aVboId,vArray.getDataBuffer)
    
    return true;
  }
  
  def updateFaceData(gl:GL2,id:Int,fromMesh:Mesh):Boolean = {
    //Retrieve the mesh that was registered previously
    val meshInfoOpt = meshRegistry.get(id)
    if(meshInfoOpt isEmpty) return false;
    val meshInfo = meshInfoOpt.get
    
    updateBufferData(gl,GL_ELEMENT_ARRAY_BUFFER,meshInfo.faceVboId,fromMesh.getFacesBuffer)

    return true;
  }
  
  /**
   * Creates all the vbo's necessary to render the mesh
   */
  private def createMeshBuffers(gl:GL2,mesh:Mesh,glUpdateFreq:Int):(Map[ArrayType,VertexArrayInfo],Int) = {
        //Create buffer objects for each vertex array and return a map of array Type -> vboID
    val vboMap = initializeVertexBuffers(gl, mesh, glUpdateFreq);
    
    //Create a buffer object for the faces array
    val faceBOId = initializeFaceBuffer(gl, mesh, glUpdateFreq)
    
    (vboMap,faceBOId)
  }
  
  /**
   * frees up all vbo's associated with the mesh
   */
  private def deleteMeshBuffers(gl:GL2,meshInfo:MeshInfo){
    //Delete the vertex buffer objects
    for(aType <- meshInfo.arrayInfoMap.keySet){
      deleteBuffer(gl,meshInfo.arrayInfoMap(aType).vboId)
    }
    
    //Delete teh face buffer object
    deleteBuffer(gl,meshInfo.faceVboId)
  }
  
  /**
   * Checks to see if the current mesh structure is different from the new mesh
   */
  private def meshDifferent(meshInfo:MeshInfo,mesh:Mesh):Boolean = {
    if(meshInfo.faces * meshInfo.vertsPerFace != mesh.faces * mesh.verticesPerFace)
      return false
    
    val arrayInfoMap = meshInfo.arrayInfoMap
    val aTypes = mesh.getAvailableArrayTypes
    
    //Quick check to make sure the number of vertex data arrays is the same as before
    if(arrayInfoMap.size != aTypes.size)
      return false
    
    //Check each vertex array to make sure they are the same types and lengths
    for(aType <- aTypes){
      val vertData = mesh(aType)
      if(arrayInfoMap.get(aType).isEmpty)
        return false
      
      val vertInfo = arrayInfoMap(aType)
      
      if(vertData.arraySize != vertInfo.verts * vertInfo.pPVertex)
        return false
    }
    
    return true;
  }
  
  /**
   * Initializes all vbo's for the vertex arrays in the mesh
   */
  private def initializeVertexBuffers(gl:GL2, mesh:Mesh, updateFrequency:Int):Map[ArrayType,VertexArrayInfo] = {
    import gl._
    
    var vboMap:Map[ArrayType,VertexArrayInfo] = Map()
    
    for(vertData <- mesh.getAvailableArrayTypes){
      vboMap += ((vertData,initializeVertexBuffer(gl,mesh(vertData),updateFrequency)))
    }
    
    vboMap
  }

  /**
   * Creates a buffer for the vertex array type and copies its data into video memory
   */
  private def initializeVertexBuffer(gl:GL2,vertData:VertexData, updateFrequency:Int):VertexArrayInfo ={
    val sizeInBytes =  vertData.verts * vertData.partsPerVertex * 4
    
    val vboId = createBuffer(gl)
    initBufferData(gl,GL_ARRAY_BUFFER,vboId,vertData.getDataBuffer,sizeInBytes,updateFrequency)
    
    new VertexArrayInfo(vertData.aType,vertData.verts,vertData.partsPerVertex,vboId)
  }
  
  /**
   * Creates a buffer for the face index array and copies its data into video memory
   */
  private def initializeFaceBuffer(gl:GL2, mesh:Mesh, updateFrequency:Int):Int = {
    val sizeInBytes = mesh.faces * mesh.verticesPerFace * 4
    
    val vboId = createBuffer(gl)
    initBufferData(gl,GL_ELEMENT_ARRAY_BUFFER,vboId,mesh.getFacesBuffer,sizeInBytes,updateFrequency)
    
    vboId
  }
  
  /**
   * Creates a new VBO
   */
  private def createBuffer(gl:GL2):Int = {
    val vboId:Array[Int] = new Array(1)
    gl.glGenBuffers(1, vboId, 0);
    
    vboId(0)
  }
  
  /**
   * Copies the data for a VBO into video memory
   */
  private def initBufferData(gl:GL2,target:Int,vboId:Int,data:FloatBuffer,sizeInBytes:Int,updateFrequency:Int) = {
    data.position(0)
    gl.glBindBuffer(target,vboId)
    gl.glBufferData(target, 
                       sizeInBytes,  
                       null, 
                       updateFrequency);
    updateBufferData(gl,target,vboId,data)
  }
  
  /**
   * Copies the data for a VBO into video memory
   */
  private def initBufferData(gl:GL2,target:Int,vboId:Int,data:IntBuffer,sizeInBytes:Int,updateFrequency:Int) = {
    data.position(0)
    gl.glBindBuffer(target,vboId)
    gl.glBufferData(target, 
                       sizeInBytes,  
                       null, 
                       updateFrequency);
    updateBufferData(gl,target,vboId,data)
  }
  
  /**
   * Copies the data for a VBO into video memory
   */
  private def updateBufferData(gl:GL2,target:Int,vboId:Int,data:FloatBuffer) = {
    data.position(0)
    gl.glBindBuffer(target,vboId)
    
    val buff = gl.glMapBuffer(target,GL_WRITE_ONLY).order(ByteOrder.nativeOrder).asFloatBuffer
    
    data.limit(buff.limit)
    buff.put(data)
    
	gl.glUnmapBuffer(target)
  }
  
  /**
   * Copies the data for a VBO into video memory
   */
  private def updateBufferData(gl:GL2,target:Int,vboId:Int,data:IntBuffer) = {
    data.position(0)
    gl.glBindBuffer(target,vboId)
    
    val buff = gl.glMapBuffer(target,GL_WRITE_ONLY).order(ByteOrder.nativeOrder).asIntBuffer
    
    data.limit(buff.limit)
    buff.put(data)
    
	gl.glUnmapBuffer(target)
  }
  
  /**
   * Frees the memory held by the vbo from video memory
   */
  private def deleteBuffer(gl:GL2,vboId:Int) {
    gl.glDeleteBuffers(1, Array(vboId), 0);
  }
  
  /**
   * Convenience method to transform from our notion of update frequency to OpenGL's version
   */
  private def getOpenGLUpdateFrequency(updateFrequency:UpdateFrequency):Int = {
    updateFrequency match{
      case Rarely => GL_STATIC_DRAW
      case Frequently => GL_DYNAMIC_DRAW
      case EachFrame => GL_STREAM_DRAW
      case _ => GL_STATIC_DRAW
    }
  }
  
  /**
   * Enable OpenGL client states for each type of vertex array
   */
  private def enableAvailableClientStates(gl:GL2,meshInfo:MeshInfo) = {
    val typeMap = meshInfo.arrayInfoMap
    for(aType <- typeMap.keySet){
      val vbo = typeMap(aType)
      aType match{
        case Vertex => enableArray(gl, GL_VERTEX_ARRAY, vbo, gl.glVertexPointer) 
        case Normal => enableArray3F(gl, GL_NORMAL_ARRAY, vbo, gl.glNormalPointer)
        case Color =>  enableArray(gl, GL_COLOR_ARRAY, vbo, gl.glColorPointer)
        case TexCoord1 => gl.glClientActiveTexture(GL_TEXTURE0)
          		enableArray(gl, GL_TEXTURE_COORD_ARRAY, vbo, gl.glTexCoordPointer)
        case TexCoord2 => gl.glClientActiveTexture(GL_TEXTURE1)
          		enableArray(gl, GL_TEXTURE_COORD_ARRAY, vbo, gl.glTexCoordPointer)
        case TexCoord3 => gl.glClientActiveTexture(GL_TEXTURE2)
          		enableArray(gl, GL_TEXTURE_COORD_ARRAY, vbo, gl.glTexCoordPointer)
        case TexCoord4 => gl.glClientActiveTexture(GL_TEXTURE3)
          		enableArray(gl, GL_TEXTURE_COORD_ARRAY, vbo, gl.glTexCoordPointer)
        case _ =>
      }
    }
  }
  
  /**
   * Disable OpenGL client states for each type of vertex array
   */
  private def disableEnabledClientStates(gl:GL2,meshInfo:MeshInfo) = {
    val typeMap = meshInfo.arrayInfoMap
    for(aType <- typeMap.keySet){
      val vbo = typeMap(aType)
      aType match {
        case Vertex => gl.glDisableClientState(GL_VERTEX_ARRAY)
        case Normal => gl.glDisableClientState(GL_NORMAL_ARRAY)
        case Color => gl.glDisableClientState(GL_COLOR_ARRAY)
        case TexCoord1 => gl.glClientActiveTexture(GL_TEXTURE0); gl.glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        case TexCoord2 => gl.glClientActiveTexture(GL_TEXTURE1); gl.glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        case TexCoord3 => gl.glClientActiveTexture(GL_TEXTURE2); gl.glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        case TexCoord4 => gl.glClientActiveTexture(GL_TEXTURE3); gl.glDisableClientState(GL_TEXTURE_COORD_ARRAY)
        case _ =>
      }
    }
  }
  
  //Setup/enable the specified array
  private def enableArray(gl:GL2, arrayType:Int, vertInfo:VertexArrayInfo, glFunc:(Int,Int,Int,Long)=>Unit):Unit = {
    import gl._

    glEnableClientState(arrayType)
    glBindBuffer(GL_ARRAY_BUFFER,vertInfo.vboId)
    glFunc(vertInfo.pPVertex,GL_FLOAT,0,0)
  }
  
  //A special case of the above function, where the glFunc only takes 3 arguments
  private def enableArray3F(gl:GL2, arrayType:Int, vertInfo:VertexArrayInfo, glFunc:(Int,Int,Long)=>Unit):Unit = {
    import gl._
    
    glBindBuffer(GL_ARRAY_BUFFER,vertInfo.vboId)
    glEnableClientState(arrayType)
    glFunc(GL_FLOAT,0,0)
  }
  
  
}
